// The purpose of the `Content` object is to abstract away the data conversions
// to and from raw content entities as strings. For example, you want to be able
// to pass in a Javascript object and have it be automatically converted into a
// JSON string if the `content-type` is set to a JSON-based media type.
// Conversely, you want to be able to transparently get back a Javascript object
// in the response if the `content-type` is a JSON-based media-type.

// One limitation of the current implementation is that it [assumes the `charset` is UTF-8](https://github.com/spire-io/shred/issues/5).

// The `Content` constructor takes an options object, which *must* have either a
// `body` or `data` property and *may* have a `type` property indicating the
// media type. If there is no `type` attribute, a default will be inferred.
var Content = function (options) {
    this.body = options.body;
    this.data = options.data;
    this.type = options.type;
};

Content.prototype = {
    // Treat `toString()` as asking for the `content.body`. That is, the raw content entity.
    //
    //     toString: function() { return this.body; }
    //
    // Commented out, but I've forgotten why. :/
};


// `Content` objects have the following attributes:
Object.defineProperties(Content.prototype, {

// - **type**. Typically accessed as `content.type`, reflects the `content-type`
//   header associated with the request or response. If not passed as an options
//   to the constructor or set explicitly, it will infer the type the `data`
//   attribute, if possible, and, failing that, will default to `text/plain`.
    type: {
        get: function () {
            if (this._type) {
                return this._type;
            } else {
                if (this._data) {
                    switch (typeof this._data) {
                        case "string":
                            return "text/plain";
                        case "object":
                            return "application/json";
                    }
                }
            }
            return "text/plain";
        },
        set: function (value) {
            this._type = value;
            return this;
        },
        enumerable: true
    },

// - **data**. Typically accessed as `content.data`, reflects the content entity
//   converted into Javascript data. This can be a string, if the `type` is, say,
//   `text/plain`, but can also be a Javascript object. The conversion applied is
//   based on the `processor` attribute. The `data` attribute can also be set
//   directly, in which case the conversion will be done the other way, to infer
//   the `body` attribute.
    data: {
        get: function () {
            if (this._body) {
                return this.processor.parser(this._body);
            } else {
                return this._data;
            }
        },
        set: function (data) {
            if (this._body && data) Errors.setDataWithBody(this);
            this._data = data;
            return this;
        },
        enumerable: true
    },

// - **body**. Typically accessed as `content.body`, reflects the content entity
//   as a UTF-8 string. It is the mirror of the `data` attribute. If you set the
//   `data` attribute, the `body` attribute will be inferred and vice-versa. If
//   you attempt to set both, an exception is raised.
    body: {
        get: function () {
            if (this._data) {
                return this.processor.stringify(this._data);
            } else {
                return this._body.toString();
            }
        },
        set: function (body) {
            if (this._data && body) Errors.setBodyWithData(this);
            this._body = body;
            return this;
        },
        enumerable: true
    },

// - **processor**. The functions that will be used to convert to/from `data` and
//   `body` attributes. You can add processors. The two that are built-in are for
//   `text/plain`, which is basically an identity transformation and
//   `application/json` and other JSON-based media types (including custom media
//   types with `+json`). You can add your own processors. See below.
    processor: {
        get: function () {
            var processor = Content.processors[this.type];
            if (processor) {
                return processor;
            } else {
                // Return the first processor that matches any part of the
                // content type. ex: application/vnd.foobar.baz+json will match json.
                var main = this.type.split(";")[0];
                var parts = main.split(/\+|\//);
                for (var i = 0, l = parts.length; i < l; i++) {
                    processor = Content.processors[parts[i]]
                }
                return processor || {parser: identity, stringify: toString};
            }
        },
        enumerable: true
    },

// - **length**. Typically accessed as `content.length`, returns the length in
//   bytes of the raw content entity.
    length: {
        get: function () {
            if (typeof Buffer !== 'undefined') {
                return Buffer.byteLength(this.body);
            }
            return this.body.length;
        }
    }
});

Content.processors = {};

// The `registerProcessor` function allows you to add your own processors to
// convert content entities. Each processor consists of a Javascript object with
// two properties:
// - **parser**. The function used to parse a raw content entity and convert it
//   into a Javascript data type.
// - **stringify**. The function used to convert a Javascript data type into a
//   raw content entity.
Content.registerProcessor = function (types, processor) {

// You can pass an array of types that will trigger this processor, or just one.
// We determine the array via duck-typing here.
    if (types.forEach) {
        types.forEach(function (type) {
            Content.processors[type] = processor;
        });
    } else {
        // If you didn't pass an array, we just use what you pass in.
        Content.processors[types] = processor;
    }
};

// Register the identity processor, which is used for text-based media types.
var identity = function (x) {
        return x;
    }
    , toString = function (x) {
        return x.toString();
    }
Content.registerProcessor(
    ["text/html", "text/plain", "text"],
    {parser: identity, stringify: toString});

// Register the JSON processor, which is used for JSON-based media types.
Content.registerProcessor(
    ["application/json; charset=utf-8", "application/json", "json"],
    {
        parser: function (string) {
            return JSON.parse(string);
        },
        stringify: function (data) {
            return JSON.stringify(data);
        }
    });

var qs = require('querystring');
// Register the post processor, which is used for JSON-based media types.
Content.registerProcessor(
    ["application/x-www-form-urlencoded"],
    {parser: qs.parse, stringify: qs.stringify});

// Error functions are defined separately here in an attempt to make the code
// easier to read.
var Errors = {
    setDataWithBody: function (object) {
        throw new Error("Attempt to set data attribute of a content object " +
            "when the body attributes was already set.");
    },
    setBodyWithData: function (object) {
        throw new Error("Attempt to set body attribute of a content object " +
            "when the data attributes was already set.");
    }
}
module.exports = Content;